/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.volatileengine.renderer.lwjgl;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Stack;

import javax.vecmath.Matrix4f;

import org.lwjgl.opengl.ARBMultitexture;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GLContext;

import com.volatileengine.VolatileEngineException;
import com.volatileengine.datatypes.AbstractArray;
import com.volatileengine.datatypes.DataType;
import com.volatileengine.scene.Geometry;
import com.volatileengine.scene.SceneMesh;
import com.volatileengine.scene.Geometry.GeometryType;

/**
 * 
 * @author Administrator
 */
class LWJGLShapeMeshRenderer {

	private final int[] TYPEMAP = new int[DataType.class.getEnumConstants().length];
	private final int[] TYPEMAPU = new int[DataType.class.getEnumConstants().length];
	private final int[] SIZE = new int[DataType.class.getEnumConstants().length];
	private final Stack<Matrix4f> TRANSFORM_STACK = new Stack<Matrix4f>();

	public LWJGLShapeMeshRenderer() {
		TYPEMAP[DataType.Int.ordinal()] = GL11.GL_INT;
		TYPEMAPU[DataType.Int.ordinal()] = GL11.GL_UNSIGNED_INT;
		SIZE[DataType.Int.ordinal()] = 4;
		TYPEMAP[DataType.Byte.ordinal()] = GL11.GL_BYTE;
		TYPEMAPU[DataType.Byte.ordinal()] = GL11.GL_UNSIGNED_BYTE;
		SIZE[DataType.Byte.ordinal()] = 1;
		TYPEMAP[DataType.Float.ordinal()] = GL11.GL_FLOAT;
		TYPEMAPU[DataType.Float.ordinal()] = GL11.GL_FLOAT;
		SIZE[DataType.Float.ordinal()] = 4;
	}

	public void render(SceneMesh shape) {
		pushTransform(shape.getWorldTransform());
		loadMatrix(shape.getWorldTransform());

		Geometry geo = shape.getEncapsulatedGeometry();
		// normal array
		loadNormalData(geo.getNormals());
		// colours
		loadColourData(geo.getColours());
		// textures
		loadTextureData(geo.getTextureCoordinates());
		// vertices
		loadVertexData(geo.getVertices());
		// draw the indices
		gl12drawIndices(geo.getIndices(), geo.getVertexQuantity() - 1, convertGeometryType(geo.getGeometryType()));
		GL11.glMatrixMode(GL11.GL_MODELVIEW);
		popTransform(shape.getWorldTransform());
	}

	private void gl12drawIndices(AbstractArray<?> array, int maxIndex, int geoType) {
		if (array.getChunkSize() != array.getStride()) {
			throw new IllegalArgumentException("Array must be tightly packed");
		}

		int type = array.getType().ordinal();
		if (bindVBO(array)) {
			GL12.glDrawRangeElements(geoType, 0, maxIndex, array.getLength() * array.getChunkSize(), TYPEMAPU[type],
					array.getOffset() * SIZE[type]);
		} else {

			array.getBuffer().position(array.getOffset());
			switch (array.getType()) {
			case Int:
				GL12.glDrawRangeElements(geoType, 0, maxIndex, (IntBuffer) array.getBuffer());
				break;
			case Byte:
				GL12.glDrawRangeElements(geoType, 0, maxIndex, (ByteBuffer) array.getBuffer());
				break;
			default:
				throw new IllegalArgumentException("Unsupported array type");
			}
		}
	}

	private void loadColourData(AbstractArray<?> array) {
		if (array == null) {
			GL11.glDisableClientState(GL11.GL_COLOR_ARRAY);
			return;
		}

		GL11.glEnableClientState(GL11.GL_COLOR_ARRAY);
		int type = array.getType().ordinal();
		int stride = array.getStride() * SIZE[type];

		if (bindVBO(array)) {
			GL11.glColorPointer(array.getChunkSize(), TYPEMAPU[type], stride, array.getOffset() * SIZE[type]);
		} else {
			array.getBuffer().position(array.getOffset());
			try {
				switch (array.getType()) {
				case Float:
					GL11.glColorPointer(array.getChunkSize(), stride, (FloatBuffer) array.getBuffer());
					break;
				case Byte:
					GL11.glColorPointer(array.getChunkSize(), true, stride, (ByteBuffer) array.getBuffer());
					break;
				default:
					throw new IllegalArgumentException("Unsupported array type");
				}
			} finally {

			}
		}
	}

	private void loadNormalData(AbstractArray<?> array) {
		if (array == null) {
			GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY);
			return;
		}

		if (array.getChunkSize() != 3) {
			throw new IllegalArgumentException("Expected chunk size 3 for normals");
		}

		GL11.glEnableClientState(GL11.GL_NORMAL_ARRAY);
		int type = array.getType().ordinal();
		int stride = array.getStride() * SIZE[type];

		if (bindVBO(array)) {
			GL11.glNormalPointer(TYPEMAP[type], stride, array.getOffset() * SIZE[type]);
		} else {
			array.getBuffer().position(array.getOffset());
			switch (array.getType()) {
			case Float:
				GL11.glNormalPointer(stride, (FloatBuffer) array.getBuffer());
				break;
			case Int:
				GL11.glNormalPointer(array.getStride(), (IntBuffer) array.getBuffer());
				break;
			case Byte:
				GL11.glNormalPointer(array.getStride(), (ByteBuffer) array.getBuffer());
				break;
			default:
				throw new IllegalArgumentException("Unsupported array type");
			}
		}
	}

	private void loadVertexData(AbstractArray<?> array) {
		if (array == null) {
			GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY);
			return;
		}

		GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY);
		int type = array.getType().ordinal();

		int stride = array.getStride() * SIZE[type];

		if (bindVBO(array)) {
			GL11.glVertexPointer(array.getChunkSize(), TYPEMAP[type], stride, array.getOffset() * SIZE[type]);
		} else {
			array.getBuffer().position(array.getOffset());
			switch (array.getType()) {
			case Float:
				GL11.glVertexPointer(array.getChunkSize(), stride, (FloatBuffer) array.getBuffer());
				break;
			case Int:
				GL11.glVertexPointer(array.getChunkSize(), stride, (IntBuffer) array.getBuffer());
				break;
			default:
				throw new IllegalArgumentException("Unsupported array type");
			}
		}
	}

	private void loadTextureData(AbstractArray<?>[] arrays) {
		if (arrays == null || arrays.length == 0) {
			GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
			return;
		}

		// if (arrays.length > 1) throw new IllegalArgumentException("Only one
		// texture unit supported");

		// loop through the textures, if multi texture is available, do so
		for (int i = 0; i < arrays.length; i++) {
			if (GLContext.getCapabilities().GL_ARB_multitexture) {
				ARBMultitexture.glClientActiveTextureARB(ARBMultitexture.GL_TEXTURE0_ARB + i);
			}

			if (arrays[i] != null) {
				loadTextureData(arrays[i]);
			}

			// if not supported, break the loop
			if (GLContext.getCapabilities().GL_ARB_multitexture == false) {
				break;
			}

		}
	}

	private void loadTextureData(AbstractArray<?> array) {
		if (array == null) {
			GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
			return;
		}

		GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);

		int type = array.getType().ordinal();
		int stride = array.getStride() * SIZE[type];
		if (bindVBO(array)) {
			GL11.glTexCoordPointer(array.getChunkSize(), TYPEMAP[type], stride, array.getOffset() * SIZE[type]);
		} else {
			array.getBuffer().position(array.getOffset());
			switch (array.getType()) {
			case Float:
				GL11.glTexCoordPointer(array.getChunkSize(), stride, (FloatBuffer) array.getBuffer());
				break;
			default:
				throw new IllegalArgumentException("Unsupported array type");
			}
		}
	}

	private int convertGeometryType(GeometryType type) {
		switch (type) {
		case LINE:
			return GL11.GL_LINE;
		case POINT:
			return GL11.GL_POINT;
		case QUAD:
			return GL11.GL_QUADS;
		case TRIANGLE:
			return GL11.GL_TRIANGLES;
		default:
			throw new VolatileEngineException("Unknown Geometry Type");
		}
	}

	private boolean bindVBO(AbstractArray<?> array) {
		/*
		 * if (GLContext.getCapabilities().GL_ARB_vertex_buffer_object ==
		 * false) { array.setVBOType(Array.VBO_TYPE_DISABLED); return false; }
		 * int type = array.isElementArray() ?
		 * ARBVertexBufferObject.GL_ELEMENT_ARRAY_BUFFER_ARB :
		 * ARBVertexBufferObject.GL_ARRAY_BUFFER_ARB;
		 * 
		 * if (array.getIdentity() != 0) {
		 * ARBVertexBufferObject.glBindBufferARB(type, array.getIdentity());
		 * return true; } else { ARBVertexBufferObject.glBindBufferARB(type,
		 * 0); return false; }
		 */
		return false;
	}

	private FloatBuffer matrixBuffer = ByteBuffer.allocateDirect(16 * 4).order(ByteOrder.nativeOrder())
			.asFloatBuffer();

	private void loadMatrix(Matrix4f mat) {
		if (mat != null) {
			matrixBuffer.clear();
			matrixBuffer.put(mat.m00).put(mat.m10).put(mat.m20).put(mat.m30);
			matrixBuffer.put(mat.m01).put(mat.m11).put(mat.m21).put(mat.m31);
			matrixBuffer.put(mat.m02).put(mat.m12).put(mat.m22).put(mat.m32);
			matrixBuffer.put(mat.m03).put(mat.m13).put(mat.m23).put(mat.m33);
			matrixBuffer.rewind();
			GL11.glLoadMatrix(matrixBuffer);

		}
	}

	private void pushTransform(Matrix4f matrix) {
		if (TRANSFORM_STACK.size() != 0) {
			if (matrix != TRANSFORM_STACK.peek()) {
				TRANSFORM_STACK.push(matrix);
				GL11.glMatrixMode(GL11.GL_MODELVIEW);
				GL11.glPushMatrix();
			}
		}
	}

	private void popTransform(Matrix4f matrix) {
		if (TRANSFORM_STACK.size() != 0) {
			if (matrix == TRANSFORM_STACK.peek()) {
				TRANSFORM_STACK.pop();
				GL11.glPopMatrix();
			}
		}
	}

}
